﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.Text;
using System.Threading;
using nwrole.plugin;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition;

namespace nwrole.lib {
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]
    public class PluginManager : IServiceContract {

        /*[System.ComponentModel.Composition.ImportMany]
        IEnumerable<IPlugin> Plugins { get; set; }*/

        private List<PluginWrapper> pluginWrappers = new List<PluginWrapper>();
        private static int supervisorPort;
        private string serviceName;
        //private bool pluginsInSameFolder;
        private PluginProviderBase pluginProvider;

        private List<PluginData> PluginDatas = new List<PluginData>();

        private Object logLock = new Object();

        //private ServiceStatus serviceStatus = ServiceStatus.Stopped;

        public Guid ServiceId { get; private set; }

        private bool running;

        private bool pulseError;

        private ConcurrentQueue<LogItem> logQueue = new ConcurrentQueue<LogItem>();
        private AutoResetEvent logQueueNotifier = new AutoResetEvent(false);
        private bool processLogs = true;

        private class LogItem
        {
            public NwroleObjectType ObjectType;
            public Guid ObjectId;
            public LogLevel Level;
            public string Message;
        }

        public PluginManager(PluginProviderBase pluginProvider, Guid serviceId, string serviceName, int supervisorPort)
        {
            running = false;

            ServiceId = serviceId;
            PluginManager.supervisorPort = supervisorPort;
            this.serviceName = serviceName;
            //this.pluginsInSameFolder = pluginsInSameFolder;
            this.pluginProvider = pluginProvider;

            this.pluginProvider.SettingsChangedEvent += OnPluginSettingsChanged;

            ThreadTraceListener listen = new ThreadTraceListener();
            listen.OnTrace += OnThreadTraceMsg;

            Trace.Listeners.Add(listen);
        }

        public Guid GetIdentifier() {
            return ServiceId;
        }

        public Result Start() {
            LogService(LogLevel.Info, string.Format("PluginManager Starting (service: {0})", serviceName), null);
            //Configuration.Load();

            running = true;
            
            new Thread(ProcessLogQueue).Start();

            PluginDatas.Clear();

            pluginProvider.RefreshPlugins();

            /*var catalog = new AggregateCatalog();
            var container = new CompositionContainer(catalog);
            var batch = new CompositionBatch();
            batch.AddPart(this);
            catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
            catalog.Catalogs.Add(new DirectoryCatalog(ServicePath));
            container.Compose(batch);*/

            IPlugin[] Plugins = pluginProvider.GetPlugins();

            LogService(LogLevel.Info, "PluginManager num plugins: " + Plugins.Count(), null);

            //bool configChanged = false;

            LoadConfig();

            //IPlugin[] testPlugins = pluginProvider.GetPlugins();

            foreach (var plugin in Plugins) {
                LogService(LogLevel.Info, "PluginManager Plugin class: " + plugin.GetType().FullName, null);

                PluginData pluginData = PluginDatas.SingleOrDefault(p => p.ClassName == plugin.GetType().FullName);

                if (pluginData == null) {
                    pluginData = new PluginData() { Id = Guid.NewGuid(), ClassName = plugin.GetType().FullName, Enabled = false };
                    pluginData.Name = pluginData.ClassName;
                    pluginData.DisplayName = pluginData.DisplayName ?? pluginData.ClassName;
                    pluginData.RestartWhenFaulted = false;
                    PluginDatas.Add(pluginData);
                    //configChanged = true;
                }

                PluginWrapper pluginWrapper = new PluginWrapper(plugin, pluginData, this);
                pluginWrappers.Add(pluginWrapper);
                //plugin.LogEvent += OnPluginLog;

                
                /*foreach (PropertyInfo propertyInfo in plugin.GetType().GetProperties()) {
                    foreach(Attribute attr in propertyInfo.GetCustomAttributes(true)) {
                        NwroleConfigAttribute configAttr = attr as NwroleConfigAttribute;
                        if(configAttr != null) {
                            Console.WriteLine("Config name: " + configAttr.Name + " property name: " + propertyInfo.Name);
                        }
                    }
                }*/

                RegisterPlugin(pluginData);

                if (pluginData.Enabled) {
                    pluginWrapper.Start();

                    LogService(LogLevel.Info, "PluginManager Plugin started: " + plugin.GetType().FullName, null);
                }
            }

            //if (configChanged) {
                SaveConfig();
            //}

            //serviceStatus = ServiceStatus.Running;

            new Thread(PulseThread).Start();

            LogService(LogLevel.Info, "PluginManager started", null);

            return Result.Success;
        }

        public Result Stop()
        {
            running = false;

            SaveConfig();

            foreach (var pluginWrapper in pluginWrappers) {
                pluginWrapper.Stop();
            }

            //serviceStatus = ServiceStatus.Stopped;

            processLogs = false;
            logQueueNotifier.Set();
            
            return Result.Success;
        }

        private void PulseThread()
        {
            while(running)
            {
                PulseService(ServiceId, serviceName, null);

                Thread.Sleep(5000);
            }
        }

        /*public ServiceStatus GetServiceStatus()
        {
            return serviceStatus;
        }*/

        private void LoadConfig() {
            PropertyList plist = pluginProvider.LoadPluginSettings();

            if (plist != null) {
                IPlugin[] Plugins = pluginProvider.GetPlugins();

                //if (File.Exists(ConfigPath)) {
                //PropertyList plist = new PropertyList(ConfigPath);

                IList<object> plugins = plist.ArrayForKey("Plugins");

                foreach (object obj in plugins) {
                    IDictionary dict = (IDictionary) obj;
                    PluginData pluginData;
                    try
                    {
                        pluginData = StorageUtil.PluginDataFromDictionary(dict);
                    }
                    catch(Exception e)
                    {
                        LogService(LogLevel.Error, "PluginManager LoadConfig() Error reading plugin data index: " + PluginDatas.Count, e);
                        throw e;
                    }

                    PluginDatas.Add(pluginData);

                    var plugin = Plugins.SingleOrDefault(p => p.GetType().FullName.Equals(pluginData.ClassName));
                    if (plugin != null) {
                        foreach (PropertyInfo propertyInfo in plugin.GetType().GetProperties()) {
                            foreach (Attribute attr in propertyInfo.GetCustomAttributes(true)) {
                                NwroleConfigAttribute configAttr = attr as NwroleConfigAttribute;
                                if (configAttr != null) {
                                    string configKey = "ConfigProp_" + configAttr.Name;
                                    if (dict.Contains(configKey)) {
                                        propertyInfo.SetValue(plugin, dict[configKey], null);
                                    }
                                }
                            }
                        }
                    }
                }
                //}
            }
        }

        private void SaveConfig() {
            IPlugin[] Plugins = pluginProvider.GetPlugins();

            var plugins = new List<object>();

            foreach (PluginData pluginData in PluginDatas) {
                IDictionary dict = StorageUtil.PluginDataToDictionary(pluginData);

                var plugin = Plugins.SingleOrDefault(p => p.GetType().FullName.Equals(pluginData.ClassName));
                if (plugin != null) {
                    foreach (PropertyInfo propertyInfo in plugin.GetType().GetProperties()) {
                        foreach (Attribute attr in propertyInfo.GetCustomAttributes(true)) {
                            NwroleConfigAttribute configAttr = attr as NwroleConfigAttribute;
                            if (configAttr != null) {
                                string configKey = "ConfigProp_" + configAttr.Name;

                                object propValue = propertyInfo.GetValue(plugin, null);
                                dict.Add(configKey, propValue);
                            }
                        }
                    }
                }

                plugins.Add(dict);
            }

            PropertyList plist = new PropertyList();
            plist["Plugins"] = plugins;

            //plist.Save(ConfigPath);

            pluginProvider.SavePluginSettings(plist);
        }

        private void OnPluginSettingsChanged(object sender, EventArgs e)
        {
            LogService(LogLevel.Info, "Plugin settings changed", null);
        }

        /*private void ConnectToSupervisor() {
            EndpointAddress address = new EndpointAddress("net.tcp://localhost:" + supervisorPort);
            NetTcpBinding binding = new NetTcpBinding();
            binding.ReceiveTimeout = new TimeSpan(0, 0, 120);
            binding.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max;
            binding.MaxReceivedMessageSize = 2147483647;
            ChannelFactory<ISupervisorContract> factory = new ChannelFactory<ISupervisorContract>(binding, address);
            factory.Credentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
            manager = factory.CreateChannel();
        }*/

        
        public Result StartPlugin(Guid pluginId)
        {
            PluginWrapper wrapper = pluginWrappers.SingleOrDefault(w => w.PluginData.Id == pluginId);
            if (wrapper == null)
                return Result.PluginNotFound;

            wrapper.Start();

            return Result.Success;
        }

        public Result StopPlugin(Guid pluginId)
        {
            PluginWrapper wrapper = pluginWrappers.SingleOrDefault(w => w.PluginData.Id == pluginId);
            if (wrapper == null)
                return Result.PluginNotFound;

            wrapper.Stop();

            return Result.Success;
        }

        public Result RestartPlugin(Guid pluginId)
        {
            PluginWrapper wrapper = pluginWrappers.SingleOrDefault(w => w.PluginData.Id == pluginId);
            if (wrapper == null)
                return Result.PluginNotFound;

            wrapper.Stop();
            wrapper.Start();

            return Result.Success;
        }

        public PluginData[] GetPluginDatas()
        {
            return PluginDatas.ToArray();
        }

        public PluginState? GetPluginState(Guid pluginId)
        {
            PluginWrapper wrapper = pluginWrappers.SingleOrDefault(w => w.PluginData.Id == pluginId);
            if (wrapper == null)
                return null;

            return wrapper.PluginState;
        }

        private TcpChannel<ISupervisorContract> GetSupervisorChannel()
        {
            var supervisor = new TcpChannel<ISupervisorContract>("localhost:" + supervisorPort + "/Supervisor");
            return supervisor;
        }

        private void RegisterPlugin(PluginData pluginData) {
            if (supervisorPort > 0) {
                lock (logLock) {
                    using (var supervisor = GetSupervisorChannel()) {
                        try {
                            supervisor.Channel.RegisterPlugin(ServiceId, pluginData.Id, pluginData.Name);
                        } catch (Exception e) {
                            LogService(LogLevel.Error, "PluginManager RegisterPlugin Error pluginId: " + pluginData.Id + " displayName: " + pluginData.DisplayName + " supervisorPort:" + supervisorPort, e);
                            EventLogger.LogException("PluginManager RegisterPlugin: " + pluginData.Id + " displayName: " + pluginData.DisplayName + " supervisorPort:" + supervisorPort + " exception: ", e);
                        }
                    }
                }
            }
        }

        private void OnThreadTraceMsg(object sender, ThreadTraceEventArgs args)
        {
            PluginWrapper wrapper = pluginWrappers.SingleOrDefault(w => w.Thread != null && w.Thread.ManagedThreadId.ToString().Equals(args.EventCache.ThreadId));

            LogLevel logLevel;
            switch (args.EventType)
            {
                case TraceEventType.Error:
                case TraceEventType.Critical:
                    logLevel = LogLevel.Error;
                    break;
                case TraceEventType.Warning:
                    logLevel = LogLevel.Warning;
                    break;
                case TraceEventType.Information:
                case TraceEventType.Verbose:
                    logLevel = LogLevel.Info;
                    break;
                default:
                    logLevel = LogLevel.Info;
                    break;
            }

            if (wrapper != null)
                Log(NwroleObjectType.Plugin, wrapper.PluginData.Id, logLevel, wrapper.PluginData.DisplayName + ": " + args.Message, null);
            else
                Log(NwroleObjectType.Service, ServiceId, logLevel, args.Message, null);
        }

        public void LogService(LogLevel level, string message, Exception ex) {
            Log(NwroleObjectType.Service, ServiceId, level, message, ex);
        }

        public void Log(NwroleObjectType objectType, Guid objectId, LogLevel level, string message, Exception ex) {
            if (ex == null)
                InternalWriteLog("Log: " + message);
            else
                InternalWriteLog("Log: " + message + " Exception: " + ex);

            if (supervisorPort > 0) {
                EnqueueLogItem(new LogItem() { Level = level, Message = message + " " + ex, ObjectId = objectId, ObjectType = objectType });

                /*lock (logLock) {
                    using (var supervisor = GetSupervisorChannel()) {
                        try {
                            //supervisor.Channel.Log(objectType, objectId, level, message, ex); // Seems like this crashes sometimes during serialization of the exception
                            supervisor.Channel.Log(objectType, objectId, level, message + " " + ex, null);
                        } catch (Exception e) {
                            WriteLog("PluginManager Log port:" + supervisorPort + " exception: " + e.ToString());
                            EventLogger.LogException("PluginManager Log port:" + supervisorPort + " exception: ", e);
                        }
                    }
                }*/
            }
        }

        private void EnqueueLogItem(LogItem logItem)
        {
            logQueue.Enqueue(logItem);
            logQueueNotifier.Set();
        }

        private void ProcessLogQueue()
        {
            processLogs = true;

            while (processLogs)
            {
                try
                {
                    if (logQueue.IsEmpty)
                        logQueueNotifier.WaitOne();

                    LogItem logItem;
                    if (logQueue.TryDequeue(out logItem))
                        ProcessLogItem(logItem);
                }
                catch (Exception e)
                {
                    WriteLog("PluginManager ProcessLogQueue exception: " + e);
                }
            }
        }

        private void ProcessLogItem(LogItem logItem)
        {
            using (var supervisor = GetSupervisorChannel())
            {
                try
                {
                    //supervisor.Channel.Log(objectType, objectId, level, message, ex); // Seems like this crashes sometimes during serialization of the exception
                    supervisor.Channel.Log(logItem.ObjectType, logItem.ObjectId, logItem.Level, logItem.Message, null);
                }
                catch (Exception e)
                {
                    WriteLog("PluginManager ProcessLogItem port:" + supervisorPort + " exception: " + e);
                    EventLogger.LogException("PluginManager ProcessLogItem port:" + supervisorPort + " exception: ", e);
                }
            }
        }

        private void PulseService(Guid id, string serviceName, string message)
        {
            if (supervisorPort > 0)
            {
                lock (logLock)
                {
                    using (var supervisor = GetSupervisorChannel())
                    {
                        try
                        {
                            supervisor.Channel.PulseService(id, serviceName, message);
                            pulseError = false;
                        }
                        catch (Exception e)
                        {
                            if(!pulseError)
                            {
                                LogService(LogLevel.Error, "PluginManager PulseService Error serviceId: " + id + " serviceName: " + serviceName + " message: " + message + " supervisorPort:" + supervisorPort, e);
                                pulseError = true;
                            }
                            
                        }
                    }
                }
            }
        }

        public void PulsePlugin(Guid id, string pluginName, string message)
        {
            if (supervisorPort > 0)
            {
                lock (logLock)
                {
                    using (var supervisor = GetSupervisorChannel())
                    {
                        try
                        {
                            supervisor.Channel.PulsePlugin(id, pluginName, message);
                            pulseError = false;
                        }
                        catch (Exception e)
                        {
                            if(!pulseError)
                            {
                                LogService(LogLevel.Error, "PluginManager PulsePlugin Error pluginId: " + id + " pluginName: " + pluginName + " message: " + message + " supervisorPort:" + supervisorPort, e);
                                pulseError = true;
                            }
                        }
                    }
                }
            }
        }

        private static void WriteLog(string content)
        {
            try
            {
                string logDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "log/PluginService/");
                if (!System.IO.Directory.Exists(logDir))
                    System.IO.Directory.CreateDirectory(logDir);

                DateTime now = DateTime.Now;

                System.IO.File.AppendAllText(logDir + "log " + now.ToString("yyyy-MM-dd") + ".txt", now.ToString("yyyy-MM-dd HH:mm:ss ") + content + "\r\n");
            }
            catch
            {
            }
        }

        private void InternalWriteLog(string content)
        {
            lock (logLock)
            {
                WriteLog(content);
            }
        }

        public void SendFaultEmail(string subject, string message)
        {
            if (supervisorPort > 0)
            {
                lock (logLock)
                {
                    using (var supervisor = GetSupervisorChannel())
                    {
                        try
                        {
                            supervisor.Channel.EmailResponsible(subject, message);
                        }
                        catch (Exception e)
                        {
                            LogService(LogLevel.Error, "PluginManager SendFaultEmail Error", e);
                        }
                    }
                }
            }
        }

        /*public void OnPluginLog(object sender, PluginLogEventArgs e) {
            //WriteLog("OnPluginLog isError:" + e.IsError + " msg:" + e.Message);
            if (e.IsError) {
                if (supervisorPort > 0) {
                    using (var supervisor = new TcpChannel<ISupervisorContract>("localhost:" + supervisorPort + "/Supervisor")) {
                        WriteLog("SupervisorContract");
                        try {
                            supervisor.Channel.ReportError(e.Message);
                        } catch (Exception ex) {
                            WriteLog("SupervisorContract OnPluginLog port:" + supervisorPort + " exception: " + ex.ToString());
                            EventLogger.LogException("OnPluginLog port:" + supervisorPort + " exception: ", ex);
                        }
                    }
                }
            }
        }*/

        /*public static void LogMessage(string message, int supervisorPort) {
          if (supervisorPort > 0) {
            using (var supervisor = new TcpChannel<ISupervisorContract>("localhost:" + supervisorPort + "/Supervisor")) {
              WriteLog("SupervisorContract");
              try {
                supervisor.Channel.ReportError(message);
              } catch (Exception ex) {
                WriteLog("SupervisorContract OnPluginLog port:" + supervisorPort + " exception: " + ex.ToString());
                EventLogger.LogException("OnPluginLog port:" + supervisorPort + " exception: ", ex);
              }
            }
          }
        }*/
    }
}
